/*
  core.js
  ImageMapster core
*/

(function ($) {
  'use strict';

  var mapster_version = '<%= pkg.version %>';

  // all public functions in $.mapster.impl are methods
  $.fn.mapster = function (method) {
    var m = $.mapster.impl;
    if ($.mapster.utils.isFunction(m[method])) {
      return m[method].apply(this, Array.prototype.slice.call(arguments, 1));
    } else if (typeof method === 'object' || !method) {
      return m.bind.apply(this, arguments);
    } else {
      $.error('Method ' + method + ' does not exist on jQuery.mapster');
    }
  };

  $.mapster = {
    version: mapster_version,
    render_defaults: {
      isSelectable: true,
      isDeselectable: true,
      fade: false,
      fadeDuration: 150,
      fill: true,
      fillColor: '000000',
      fillColorMask: 'FFFFFF',
      fillOpacity: 0.7,
      highlight: true,
      stroke: false,
      strokeColor: 'ff0000',
      strokeOpacity: 1,
      strokeWidth: 1,
      includeKeys: '',
      altImage: null,
      altImageId: null, // used internally
      altImages: {},
      altImageOpacity: null
    },
    defaults: {
      clickNavigate: false,
      navigateMode: 'location', // location|open
      wrapClass: null,
      wrapCss: null,
      onGetList: null,
      sortList: false,
      // listenToList: false, // not used - see mapdata.js line 1002
      mapKey: '',
      mapValue: '',
      singleSelect: false,
      listKey: 'value',
      listSelectedAttribute: 'selected',
      listSelectedClass: null,
      onClick: null,
      onMouseover: null,
      onMouseout: null,
      mouseoutDelay: 0,
      onStateChange: null,
      boundList: null,
      onConfigured: null,
      configTimeout: 30000,
      noHrefIsMask: true,
      scaleMap: true,
      scaleMapBounds: { below: 0.98, above: 1.02 },
      enableAutoResizeSupport: false, // TODO: Remove in next major release
      autoResize: false,
      autoResizeDelay: 0,
      autoResizeDuration: 0,
      onAutoResize: null,
      areas: []
    },
    shared_defaults: {
      render_highlight: { fade: true },
      render_select: { fade: false },
      staticState: null,
      selected: null
    },
    area_defaults: {
      includeKeys: '',
      isMask: false
    },
    canvas_style: {
      position: 'absolute',
      left: 0,
      top: 0,
      padding: 0,
      border: 0
    },
    hasCanvas: null,
    map_cache: [],
    hooks: {},
    addHook: function (name, callback) {
      this.hooks[name] = (this.hooks[name] || []).push(callback);
    },
    callHooks: function (name, context) {
      $.each(this.hooks[name] || [], function (_, e) {
        e.apply(context);
      });
    },
    utils: {
      when: {
        all: function (deferredArray) {
          // TODO: Promise breaks ES5 support
          // eslint-disable-next-line no-undef
          return Promise.all(deferredArray);
        },
        defer: function () {
          // Deferred is frequently referred to as an anti-pattern largely
          // due to error handling, however to avoid reworking existing
          // APIs and support backwards compat, creating a "deferred"
          // polyfill via native promise
          var Deferred = function () {
            // TODO: Promise breaks ES5 support
            // eslint-disable-next-line no-undef
            this.promise = new Promise(
              function (resolve, reject) {
                this.resolve = resolve;
                this.reject = reject;
              }.bind(this)
            );

            this.then = this.promise.then.bind(this.promise);
            this.catch = this.promise.catch.bind(this.promise);
          };
          return new Deferred();
        }
      },
      defer: function () {
        return this.when.defer();
      },
      // extends the constructor, returns a new object prototype. Does not refer to the
      // original constructor so is protected if the original object is altered. This way you
      // can "extend" an object by replacing it with its subclass.
      subclass: function (BaseClass, constr) {
        var Subclass = function () {
          var me = this,
            args = Array.prototype.slice.call(arguments, 0);
          me.base = BaseClass.prototype;
          me.base.init = function () {
            BaseClass.prototype.constructor.apply(me, args);
          };
          constr.apply(me, args);
        };
        Subclass.prototype = new BaseClass();
        Subclass.prototype.constructor = Subclass;
        return Subclass;
      },
      asArray: function (obj) {
        return obj.constructor === Array ? obj : this.split(obj);
      },
      // clean split: no padding or empty elements
      split: function (text, cb) {
        var i,
          el,
          arr = text.split(',');
        for (i = 0; i < arr.length; i++) {
          // backwards compat for $.trim which would return empty string on null
          // which theoertically should not happen here
          el = arr[i] ? arr[i].trim() : '';
          if (el === '') {
            arr.splice(i, 1);
          } else {
            arr[i] = cb ? cb(el) : el;
          }
        }
        return arr;
      },
      // similar to $.extend but does not add properties (only updates), unless the
      // first argument is an empty object, then all properties will be copied
      updateProps: function (_target, _template) {
        var onlyProps,
          target = _target || {},
          template = $.isEmptyObject(target) ? _template : _target;

        //if (template) {
        onlyProps = [];
        $.each(template, function (prop) {
          onlyProps.push(prop);
        });
        //}

        $.each(Array.prototype.slice.call(arguments, 1), function (_, src) {
          $.each(src || {}, function (prop) {
            if (!onlyProps || $.inArray(prop, onlyProps) >= 0) {
              var p = src[prop];

              if ($.isPlainObject(p)) {
                // not recursive - only copies 1 level of subobjects, and always merges
                target[prop] = $.extend(target[prop] || {}, p);
              } else if (p && p.constructor === Array) {
                target[prop] = p.slice(0);
              } else if (typeof p !== 'undefined') {
                target[prop] = src[prop];
              }
            }
          });
        });
        return target;
      },
      isElement: function (o) {
        return typeof HTMLElement === 'object'
          ? o instanceof HTMLElement
          : o &&
              typeof o === 'object' &&
              o.nodeType === 1 &&
              typeof o.nodeName === 'string';
      },
      /**
       * Basic indexOf implementation for IE7-8. Though we use $.inArray, some jQuery versions will try to
       * use a prototpye on the calling object, defeating the purpose of using $.inArray in the first place.
       *
       * This will be replaced with the array prototype if it's available.
       *
       * @param  {Array} arr The array to search
       * @param {Object} target The item to search for
       * @return {Number} The index of the item, or -1 if not found
       */
      indexOf: function (arr, target) {
        if (Array.prototype.indexOf) {
          return Array.prototype.indexOf.call(arr, target);
        } else {
          for (var i = 0; i < arr.length; i++) {
            if (arr[i] === target) {
              return i;
            }
          }
          return -1;
        }
      },

      // finds element of array or object with a property "prop" having value "val"
      // if prop is not defined, then just looks for property with value "val"
      indexOfProp: function (obj, prop, val) {
        var result = obj.constructor === Array ? -1 : null;
        $.each(obj, function (i, e) {
          if (e && (prop ? e[prop] : e) === val) {
            result = i;
            return false;
          }
        });
        return result;
      },
      // returns "obj" if true or false, or "def" if not true/false
      boolOrDefault: function (obj, def) {
        return this.isBool(obj) ? obj : def || false;
      },
      isBool: function (obj) {
        return typeof obj === 'boolean';
      },
      isUndef: function (obj) {
        return typeof obj === 'undefined';
      },
      isFunction: function (obj) {
        return typeof obj === 'function';
      },
      isNumeric: function (obj) {
        return !isNaN(parseFloat(obj));
      },
      // evaluates "obj", if function, calls it with args
      // (todo - update this to handle variable lenght/more than one arg)
      ifFunction: function (obj, that, args) {
        if (this.isFunction(obj)) {
          obj.call(that, args);
        }
      },
      size: function (image, raw) {
        var u = $.mapster.utils;
        return {
          width: raw
            ? image.width || image.naturalWidth
            : u.imgWidth(image, true),
          height: raw
            ? image.height || image.naturalHeight
            : u.imgHeight(image, true),
          complete: function () {
            return !!this.height && !!this.width;
          }
        };
      },

      /**
       * Set the opacity of the element. This is an IE<8 specific function for handling VML.
       * When using VML we must override the "setOpacity" utility function (monkey patch ourselves).
       * jQuery does not deal with opacity correctly for VML elements. This deals with that.
       *
       * @param {Element} el The DOM element
       * @param {double} opacity A value between 0 and 1 inclusive.
       */

      setOpacity: function (el, opacity) {
        if ($.mapster.hasCanvas()) {
          el.style.opacity = opacity;
        } else {
          $(el).each(function (_, e) {
            if (typeof e.opacity !== 'undefined') {
              e.opacity = opacity;
            } else {
              $(e).css('opacity', opacity);
            }
          });
        }
      },

      // fade "el" from opacity "op" to "endOp" over a period of time "duration"

      fader: (function () {
        var elements = {},
          lastKey = 0,
          fade_func = function (el, op, endOp, duration) {
            var index,
              cbIntervals = duration / 15,
              obj,
              u = $.mapster.utils;

            if (typeof el === 'number') {
              obj = elements[el];
              if (!obj) {
                return;
              }
            } else {
              index = u.indexOfProp(elements, null, el);
              if (index) {
                delete elements[index];
              }
              elements[++lastKey] = obj = el;
              el = lastKey;
            }

            endOp = endOp || 1;

            op =
              op + endOp / cbIntervals > endOp - 0.01
                ? endOp
                : op + endOp / cbIntervals;

            u.setOpacity(obj, op);
            if (op < endOp) {
              setTimeout(function () {
                fade_func(el, op, endOp, duration);
              }, 15);
            }
          };
        return fade_func;
      })(),
      getShape: function (areaEl) {
        // per HTML spec, invalid value and missing value default is 'rect'
        // Handling as follows:
        //   - Missing/Empty value will be treated as 'rect' per spec
        //   - Avoid handling invalid values do to perf impact
        // Note - IM currently does not support shape of 'default' so while its technically
        // a valid attribute value it should not be used.
        // https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element
        return (areaEl.shape || 'rect').toLowerCase();
      },
      hasAttribute: function (el, attrName) {
        var attr = $(el).attr(attrName);
        // For some browsers, `attr` is undefined; for others, `attr` is false.
        return typeof attr !== 'undefined' && attr !== false;
      }
    },
    getBoundList: function (opts, key_list) {
      if (!opts.boundList) {
        return null;
      }
      var index,
        key,
        result = $(),
        list = $.mapster.utils.split(key_list);
      opts.boundList.each(function (_, e) {
        for (index = 0; index < list.length; index++) {
          key = list[index];
          if ($(e).is('[' + opts.listKey + '="' + key + '"]')) {
            result = result.add(e);
          }
        }
      });
      return result;
    },
    getMapDataIndex: function (obj) {
      var img, id;
      switch (obj.tagName && obj.tagName.toLowerCase()) {
        case 'area':
          id = $(obj).parent().attr('name');
          img = $("img[usemap='#" + id + "']")[0];
          break;
        case 'img':
          img = obj;
          break;
      }
      return img ? this.utils.indexOfProp(this.map_cache, 'image', img) : -1;
    },
    getMapData: function (obj) {
      var index = this.getMapDataIndex(obj.length ? obj[0] : obj);
      if (index >= 0) {
        return index >= 0 ? this.map_cache[index] : null;
      }
    },
    /**
     * Queue a command to be run after the active async operation has finished
     * @param  {MapData}  map_data    The target MapData object
     * @param  {jQuery}   that        jQuery object on which the command was invoked
     * @param  {string}   command     the ImageMapster method name
     * @param  {object[]} args        arguments passed to the method
     * @return {bool}                 true if the command was queued, false if not (e.g. there was no need to)
     */
    queueCommand: function (map_data, that, command, args) {
      if (!map_data) {
        return false;
      }
      if (!map_data.complete || map_data.currentAction) {
        map_data.commands.push({
          that: that,
          command: command,
          args: args
        });
        return true;
      }
      return false;
    },
    unload: function () {
      this.impl.unload();
      this.utils = null;
      this.impl = null;
      $.fn.mapster = null;
      $.mapster = null;
      return $('*').off('.mapster');
    }
  };

  // Config for object prototypes
  // first: use only first object (for things that should not apply to lists)
  /// calls back one of two fuinctions, depending on whether an area was obtained.
  // opts: {
  //    name: 'method name',
  //    key: 'key,
  //    args: 'args'
  //
  //}
  // name: name of method (required)
  // args: arguments to re-call with
  // Iterates through all the objects passed, and determines whether it's an area or an image, and calls the appropriate
  // callback for each. If anything is returned from that callback, the process is stopped and that data return. Otherwise,
  // the object itself is returned.

  var m = $.mapster,
    u = m.utils,
    ap = Array.prototype;

  // jQuery's width() and height() are broken on IE9 in some situations. This tries everything.
  $.each(['width', 'height'], function (_, e) {
    var capProp = e.substr(0, 1).toUpperCase() + e.substr(1);
    // when jqwidth parm is passed, it also checks the jQuery width()/height() property
    // the issue is that jQUery width() can report a valid size before the image is loaded in some browsers
    // without it, we can read zero even when image is loaded in other browsers if its not visible
    // we must still check because stuff like adblock can temporarily block it
    // what a goddamn headache
    u['img' + capProp] = function (img, jqwidth) {
      return (
        (jqwidth ? $(img)[e]() : 0) ||
        img[e] ||
        img['natural' + capProp] ||
        img['client' + capProp] ||
        img['offset' + capProp]
      );
    };
  });

  /**
   * The Method object encapsulates the process of testing an ImageMapster method to see if it's being
   * invoked on an image, or an area; then queues the command if the MapData is in an active state.
   *
   * @param {[jQuery]}    that        The target of the invocation
   * @param {[function]}  func_map    The callback if the target is an image map
   * @param {[function]}  func_area   The callback if the target is an area
   * @param {[object]}    opt         Options: { key: a map key if passed explicitly
   *                                             name: the command name, if it can be queued,
   *                                             args: arguments to the method
   *                                            }
   */

  m.Method = function (that, func_map, func_area, opts) {
    var me = this;
    me.name = opts.name;
    me.output = that;
    me.input = that;
    me.first = opts.first || false;
    me.args = opts.args ? ap.slice.call(opts.args, 0) : [];
    me.key = opts.key;
    me.func_map = func_map;
    me.func_area = func_area;
    //$.extend(me, opts);
    me.name = opts.name;
    me.allowAsync = opts.allowAsync || false;
  };
  m.Method.prototype = {
    constructor: m.Method,
    go: function () {
      var i,
        data,
        ar,
        len,
        result,
        src = this.input,
        area_list = [],
        me = this;

      len = src.length;
      for (i = 0; i < len; i++) {
        data = $.mapster.getMapData(src[i]);
        if (data) {
          if (
            !me.allowAsync &&
            m.queueCommand(data, me.input, me.name, me.args)
          ) {
            if (this.first) {
              result = '';
            }
            continue;
          }

          ar = data.getData(src[i].nodeName === 'AREA' ? src[i] : this.key);
          if (ar) {
            if ($.inArray(ar, area_list) < 0) {
              area_list.push(ar);
            }
          } else {
            result = this.func_map.apply(data, me.args);
          }
          if (this.first || typeof result !== 'undefined') {
            break;
          }
        }
      }
      // if there were areas, call the area function for each unique group
      $(area_list).each(function (_, e) {
        result = me.func_area.apply(e, me.args);
      });

      if (typeof result !== 'undefined') {
        return result;
      } else {
        return this.output;
      }
    }
  };

  $.mapster.impl = (function () {
    var me = {},
      addMap = function (map_data) {
        return m.map_cache.push(map_data) - 1;
      },
      removeMap = function (map_data) {
        m.map_cache.splice(map_data.index, 1);
        for (var i = m.map_cache.length - 1; i >= map_data.index; i--) {
          m.map_cache[i].index--;
        }
      };

    /**
     * Test whether the browser supports VML. Credit: google.
     * http://stackoverflow.com/questions/654112/how-do-you-detect-support-for-vml-or-svg-in-a-browser
     *
     * @return {bool} true if vml is supported, false if not
     */

    function hasVml() {
      var a = $('<div />').appendTo('body');
      a.html('<v:shape id="vml_flag1" adj="1" />');

      var b = a[0].firstChild;
      b.style.behavior = 'url(#default#VML)';
      var has = b ? typeof b.adj === 'object' : true;
      a.remove();
      return has;
    }

    /**
     * Return a reference to the IE namespaces object, if available, or an empty object otherwise
     * @return {obkect} The document.namespaces object.
     */
    function namespaces() {
      return typeof document.namespaces === 'object'
        ? document.namespaces
        : null;
    }

    /**
     * Test for the presence of HTML5 Canvas support. This also checks to see if excanvas.js has been
     * loaded and is faking it; if so, we assume that canvas is not supported.
     *
     * @return {bool} true if HTML5 canvas support, false if not
     */

    function hasCanvas() {
      var d = namespaces();
      // when g_vml_ is present, then we can be sure excanvas is active, meaning there's not a real canvas.

      return d && d.g_vml_
        ? false
        : $('<canvas />')[0].getContext
          ? true
          : false;
    }

    /**
     * Merge new area data into existing area options on a MapData object. Used for rebinding.
     *
     * @param  {[MapData]} map_data     The MapData object
     * @param  {[object[]]} areas       areas array to merge
     */

    function merge_areas(map_data, areas) {
      var ar,
        index,
        map_areas = map_data.options.areas;

      if (areas) {
        $.each(areas, function (_, e) {
          // Issue #68 - ignore invalid data in areas array

          if (!e || !e.key) {
            return;
          }

          index = u.indexOfProp(map_areas, 'key', e.key);

          if (index >= 0) {
            $.extend(map_areas[index], e);
          } else {
            map_areas.push(e);
          }
          ar = map_data.getDataForKey(e.key);
          if (ar) {
            $.extend(ar.options, e);
          }
        });
      }
    }
    function merge_options(map_data, options) {
      var temp_opts = u.updateProps({}, options);
      delete temp_opts.areas;

      u.updateProps(map_data.options, temp_opts);

      merge_areas(map_data, options.areas);
      // refresh the area_option template
      u.updateProps(map_data.area_options, map_data.options);
    }

    // Most methods use the "Method" object which handles figuring out whether it's an image or area called and
    // parsing key parameters. The constructor wants:
    // this, the jQuery object
    // a function that is called when an image was passed (with a this context of the MapData)
    // a function that is called when an area was passed (with a this context of the AreaData)
    // options: first = true means only the first member of a jQuery object is handled
    //          key = the key parameters passed
    //          defaultReturn: a value to return other than the jQuery object (if its not chainable)
    //          args: the arguments
    // Returns a comma-separated list of user-selected areas. "staticState" areas are not considered selected for the purposes of this method.

    me.get = function (key) {
      var md = m.getMapData(this);
      if (!(md && md.complete)) {
        throw "Can't access data until binding complete.";
      }

      return new m.Method(
        this,
        function () {
          // map_data return
          return this.getSelected();
        },
        function () {
          return this.isSelected();
        },
        {
          name: 'get',
          args: arguments,
          key: key,
          first: true,
          allowAsync: true,
          defaultReturn: ''
        }
      ).go();
    };
    me.data = function (key) {
      return new m.Method(
        this,
        null,
        function () {
          return this;
        },
        { name: 'data', args: arguments, key: key }
      ).go();
    };

    // Set or return highlight state.
    //  $(img).mapster('highlight') -- return highlighted area key, or null if none
    //  $(area).mapster('highlight') -- highlight an area
    //  $(img).mapster('highlight','area_key') -- highlight an area
    //  $(img).mapster('highlight',false) -- remove highlight
    me.highlight = function (key) {
      return new m.Method(
        this,
        function () {
          if (key === false) {
            this.ensureNoHighlight();
          } else {
            var id = this.highlightId;
            return id >= 0 ? this.data[id].key : null;
          }
        },
        function () {
          this.highlight();
        },
        { name: 'highlight', args: arguments, key: key, first: true }
      ).go();
    };
    // Return the primary keys for an area or group key.
    // $(area).mapster('key')
    // includes all keys (not just primary keys)
    // $(area).mapster('key',true)
    // $(img).mapster('key','group-key')

    // $(img).mapster('key','group-key', true)
    me.keys = function (key, all) {
      var keyList = [],
        md = m.getMapData(this);

      if (!(md && md.complete)) {
        throw "Can't access data until binding complete.";
      }

      function addUniqueKeys(ad) {
        var areas,
          keys = [];
        if (!all) {
          keys.push(ad.key);
        } else {
          areas = ad.areas();
          $.each(areas, function (_, e) {
            keys = keys.concat(e.keys);
          });
        }
        $.each(keys, function (_, e) {
          if ($.inArray(e, keyList) < 0) {
            keyList.push(e);
          }
        });
      }

      if (!(md && md.complete)) {
        return '';
      }
      if (typeof key === 'string') {
        if (all) {
          addUniqueKeys(md.getDataForKey(key));
        } else {
          keyList = [md.getKeysForGroup(key)];
        }
      } else {
        all = key;
        this.each(function (_, e) {
          if (e.nodeName === 'AREA') {
            addUniqueKeys(md.getDataForArea(e));
          }
        });
      }
      return keyList.join(',');
    };
    me.select = function () {
      me.set.call(this, true);
    };
    me.deselect = function () {
      me.set.call(this, false);
    };

    /**
     * Select or unselect areas. Areas can be identified by a single string key, a comma-separated list of keys,
     * or an array of strings.
     *
     *
     * @param {boolean} selected Determines whether areas are selected or deselected
     * @param {string|string[]} key A string, comma-separated string, or array of strings indicating
     *                              the areas to select or deselect
     * @param {object} options Rendering options to apply when selecting an area
     */

    me.set = function (selected, key, options) {
      var lastMap,
        map_data,
        opts = options,
        key_list,
        area_list; // array of unique areas passed

      function setSelection(ar) {
        var newState = selected;
        if (ar) {
          switch (selected) {
            case true:
              ar.select(opts);
              break;
            case false:
              ar.deselect(true);
              break;
            default:
              newState = ar.toggle(opts);
              break;
          }
          return newState;
        }
      }
      function addArea(ar) {
        if (ar && $.inArray(ar, area_list) < 0) {
          area_list.push(ar);
          key_list += (key_list === '' ? '' : ',') + ar.key;
        }
      }
      // Clean up after a group that applied to the same map
      function finishSetForMap(map_data) {
        $.each(area_list, function (_, el) {
          setSelection(el);
        });
        if (!selected) {
          map_data.removeSelectionFinish();
        }
      }

      this.filter('img,area').each(function (_, e) {
        var keys;
        map_data = m.getMapData(e);

        if (map_data !== lastMap) {
          if (lastMap) {
            finishSetForMap(lastMap);
          }

          area_list = [];
          key_list = '';
        }

        if (map_data) {
          keys = '';
          if (e.nodeName.toUpperCase() === 'IMG') {
            if (!m.queueCommand(map_data, $(e), 'set', [selected, key, opts])) {
              if (key instanceof Array) {
                if (key.length) {
                  keys = key.join(',');
                }
              } else {
                keys = key;
              }

              if (keys) {
                $.each(u.split(keys), function (_, key) {
                  addArea(map_data.getDataForKey(key.toString()));
                  lastMap = map_data;
                });
              }
            }
          } else {
            opts = key;
            if (!m.queueCommand(map_data, $(e), 'set', [selected, opts])) {
              addArea(map_data.getDataForArea(e));
              lastMap = map_data;
            }
          }
        }
      });

      if (map_data) {
        finishSetForMap(map_data);
      }

      return this;
    };
    me.unbind = function (preserveState) {
      return new m.Method(
        this,
        function () {
          this.clearEvents();
          this.clearMapData(preserveState);
          removeMap(this);
        },
        null,
        { name: 'unbind', args: arguments }
      ).go();
    };

    // refresh options and update selection information.
    me.rebind = function (options) {
      return new m.Method(
        this,
        function () {
          var me = this;

          me.complete = false;
          me.configureOptions(options);
          me.bindImages().then(function () {
            me.buildDataset(true);
            me.complete = true;
            me.onConfigured();
          });
          //this.redrawSelections();
        },
        null,
        {
          name: 'rebind',
          args: arguments
        }
      ).go();
    };
    // get options. nothing or false to get, or "true" to get effective options (versus passed options)
    me.get_options = function (key, effective) {
      var eff = u.isBool(key) ? key : effective; // allow 2nd parm as "effective" when no key
      return new m.Method(
        this,
        function () {
          var opts = $.extend({}, this.options);
          if (eff) {
            opts.render_select = u.updateProps(
              {},
              m.render_defaults,
              opts,
              opts.render_select
            );

            opts.render_highlight = u.updateProps(
              {},
              m.render_defaults,
              opts,
              opts.render_highlight
            );
          }
          return opts;
        },
        function () {
          return eff ? this.effectiveOptions() : this.options;
        },
        {
          name: 'get_options',
          args: arguments,
          first: true,
          allowAsync: true,
          key: key
        }
      ).go();
    };

    // set options - pass an object with options to set,
    me.set_options = function (options) {
      return new m.Method(
        this,
        function () {
          merge_options(this, options);
        },
        null,
        {
          name: 'set_options',
          args: arguments
        }
      ).go();
    };
    me.unload = function () {
      var i;
      for (i = m.map_cache.length - 1; i >= 0; i--) {
        if (m.map_cache[i]) {
          me.unbind.call($(m.map_cache[i].image));
        }
      }
      me.graphics = null;
    };

    me.snapshot = function () {
      return new m.Method(
        this,
        function () {
          $.each(this.data, function (_, e) {
            e.selected = false;
          });

          this.base_canvas = this.graphics.createVisibleCanvas(this);
          $(this.image).before(this.base_canvas);
        },
        null,
        { name: 'snapshot' }
      ).go();
    };

    // do not queue this function

    me.state = function () {
      var md,
        result = null;
      $(this).each(function (_, e) {
        if (e.nodeName === 'IMG') {
          md = m.getMapData(e);
          if (md) {
            result = md.state();
          }
          return false;
        }
      });
      return result;
    };

    me.bind = function (options) {
      return this.each(function (_, e) {
        var img, map, usemap, md;

        // save ref to this image even if we can't access it yet. commands will be queued
        img = $(e);

        md = m.getMapData(e);

        // if already bound completely, do a total rebind

        if (md) {
          me.unbind.apply(img);
          if (!md.complete) {
            // will be queued
            return true;
          }
          md = null;
        }

        // ensure it's a valid image
        // jQuery bug with Opera, results in full-url#usemap being returned from jQuery's attr.
        // So use raw getAttribute instead.

        usemap = this.getAttribute('usemap');
        map = usemap && $('map[name="' + usemap.substr(1) + '"]');
        if (!(img.is('img') && usemap && map.length > 0)) {
          return true;
        }

        // sorry - your image must have border:0, things are too unpredictable otherwise.
        img.css('border', 0);

        if (!md) {
          md = new m.MapData(this, options);

          md.index = addMap(md);
          md.map = map;
          md.bindImages().then(function () {
            md.initialize();
          });
        }
      });
    };

    me.init = function (useCanvas) {
      var style, shapes;

      // for testing/debugging, use of canvas can be forced by initializing
      // manually with "true" or "false". But generally we test for it.

      m.hasCanvas = function () {
        if (!u.isBool(m.hasCanvas.value)) {
          m.hasCanvas.value = u.isBool(useCanvas) ? useCanvas : hasCanvas();
        }
        return m.hasCanvas.value;
      };

      m.hasVml = function () {
        if (!u.isBool(m.hasVml.value)) {
          // initialize VML the first time we detect its presence.
          var d = namespaces();

          if (d && !d.v) {
            d.add('v', 'urn:schemas-microsoft-com:vml');
            style = document.createStyleSheet();
            shapes = [
              'shape',
              'rect',
              'oval',
              'circ',
              'fill',
              'stroke',
              'imagedata',
              'group',
              'textbox'
            ];
            $.each(shapes, function (_, el) {
              style.addRule(
                'v\\:' + el,
                'behavior: url(#default#VML); antialias:true'
              );
            });
          }
          m.hasVml.value = hasVml();
        }

        return m.hasVml.value;
      };

      $.extend(m.defaults, m.render_defaults, m.shared_defaults);
      $.extend(m.area_defaults, m.render_defaults, m.shared_defaults);
    };
    me.test = function (obj) {
      return eval(obj);
    };
    return me;
  })();
  $.mapster.impl.init();
})(jQuery);
